﻿using System;
using Microsoft.SPOT;

using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using GTI = Gadgeteer.Interfaces;

namespace Gadgeteer.Modules.Seeed
{
    /// <summary>
    /// A GPS module for Microsoft .NET Gadgeteer
    /// </summary>
    public class GPS : GTM.Module
    {
        // This example implements  a driver in managed code for a simple Gadgeteer module.  The module uses a 
        // single GTI.InterruptInput to interact with a sensor that can be in either of two states: low or high.
        // The example code shows the recommended code pattern for exposing the property (IsHigh). 
        // The example also uses the recommended code pattern for exposing two events: GPSHigh, GPSLow. 
        // The triple-slash "///" comments shown will be used in the build process to create an XML file named
        // GTM.Seeed.GPS. This file will provide Intellisense and documention for the
        // interface and make it easier for developers to use the GPS module.        

        // Note: A constructor summary is auto-generated by the doc builder.
        /// <summary></summary>
        /// <param name="socketNumber">The socket that this module is plugged in to.</param>
        public GPS(int socketNumber)
        {
            // This finds the Socket instance from the user-specified socket number.  
            // This will generate user-friendly error messages if the socket is invalid.
            // If there is more than one socket on this module, then instead of "null" for the last parameter, 
            // put text that identifies the socket to the user (e.g. "S" if there is a socket type S)
            Socket socket = Socket.GetSocket(socketNumber, true, this, null);

            powerControl = new GTI.DigitalOutput(socket, Socket.Pin.Three, true, this);
            gpsExtInt = new GTI.DigitalOutput(socket, Socket.Pin.Six, true, this);

            serial = new GTI.Serial(socket, 9600, GTI.Serial.SerialParity.None, GTI.Serial.SerialStopBits.One, 8, GTI.Serial.HardwareFlowControl.NotRequired, this);
            serial.AutoReadLineEnabled = true;
            serial.LineReceivedEventDelimiter = "\r\n";
            serial.LineReceived += new GTI.Serial.LineReceivedEventHandler(serial_LineReceived);
        }

        private GTI.DigitalOutput powerControl;
        private GTI.DigitalOutput gpsExtInt;
        private GTI.Serial serial;

        private bool userSetEnabled = false;

        /// <summary>
        /// This enables or disables the GPS.  Instead of using this, you can just add and remove handlers from the PositionReceived event, which will automatically enable or disable the GPS if this property has not been manually changed.
        /// </summary>
        public bool Enabled
        {
            get
            {
                return !powerControl.Read();
            }

            set
            {
                powerControl.Write(!value);
                userSetEnabled = true;
                if (serial.IsOpen && !Enabled) serial.Close();
                if (!serial.IsOpen && Enabled) serial.Open();
            }
        }

        bool sentInvalidPosition = false;
        private TimeSpan validPositionTime = TimeSpan.MinValue;

        void serial_LineReceived(GTI.Serial sender, string line)
        {
            try
            {
                // Just looking for "RMC" messages for now
                // Example message with no position: $GPRMC,,V,,,,,,,,,,N*53 
                // Example message with position: $GPRMC,083559.00,A,4717.11437,N,00833.91522,E,0.004,77.52,091202,,,A*57
                // Message structure: $GPRMC,hhmmss,status,latitude,N,longitude,E,spd,cog,ddmmyy,mv,mvE,mode*cs

                if(line != null && line.Length > 0) OnNMEASentenceReceived(this, line);

                if (line.Substring(0, 7) != "$GPRMC,") return;
                string[] tokens = line.Split(',');
                if (tokens.Length != 13)
                {
                    DebugPrint("RMC NMEA line does not have 13 tokens, ignoring");
                    return;
                }

                DebugPrint(line);

                

                if (tokens[2] != "A")
                {
                    // Send only one invalid position to user
                    if (!sentInvalidPosition)
                    {
                        OnInvalidPositionReceived(this);
                        sentInvalidPosition = true;
                    }
                    return;
                }

                double timeRawDouble = Double.Parse(tokens[1]);

                int timeRaw = (int)timeRawDouble;
                int hours = timeRaw / 10000;
                int minutes = (timeRaw / 100) % 100;
                int seconds = timeRaw % 100;
                int milliseconds = (int)((timeRawDouble - timeRaw) * 1000.0);
                int dateRaw = Int32.Parse(tokens[9]);
                int days = dateRaw / 10000;
                int months = (dateRaw / 100) % 100;
                int years = 2000 + (dateRaw % 100);

                Position position = new Position();
                position.FixTimeUtc = new DateTime(years, months, days, hours, minutes, seconds, milliseconds);

                position.LatitudeString = tokens[3] + " " + tokens[4];
                position.LongitudeString = tokens[5] + " " + tokens[6];

                double latitudeRaw = double.Parse(tokens[3]);
                int latitudeDegreesRaw = ((int)latitudeRaw) / 100;
                double latitudeMinutesRaw = latitudeRaw - (latitudeDegreesRaw * 100);
                position.Latitude = latitudeDegreesRaw + (latitudeMinutesRaw / 60.0);
                if (tokens[4] == "S") position.Latitude = -position.Latitude;

                double longitudeRaw = double.Parse(tokens[5]);
                int longitudeDegreesRaw = ((int)longitudeRaw) / 100;
                double longitudeMinutesRaw = longitudeRaw - (longitudeDegreesRaw * 100);
                position.Longitude = longitudeDegreesRaw + (longitudeMinutesRaw / 60.0);
                if (tokens[6] == "W") position.Longitude = -position.Longitude;

                position.SpeedKnots = 0;
                if (tokens[7] != "") position.SpeedKnots = Double.Parse(tokens[7]);
                position.CourseDegrees = 0;
                if(tokens[8] != "") position.CourseDegrees = Double.Parse(tokens[8]);

                validPositionTime = GT.Timer.GetMachineTime();
                sentInvalidPosition = false;
                LastPosition = position;
                OnPositionReceived(this, position);
            }
            catch
            {
                DebugPrint("Error parsing RMC NMEA message");
            }
        }
            
        /// <summary>
        /// Represents a GPS position fix
        /// </summary>
        public class Position {
            /// <summary>
            /// The latitude
            /// </summary>
            public double Latitude { get; set; }

            /// <summary>
            /// The longitude
            /// </summary>
            public double Longitude { get; set; }

            /// <summary>
            /// A string representing the latitude, in the format ddmm.mmmm H, where dd = degrees, mm.mmm = minutes and fractional minutes, and H = hemisphere (N/S)
            /// </summary>
            public string LatitudeString { get; set; }

            /// <summary>
            /// A string representing the longitude, in the format ddmm.mmmm H, where dd = degrees, mm.mmm = minutes and fractional minutes, and H = hemisphere (E/W)
            /// </summary>
            public string LongitudeString { get; set; }

            /// <summary>
            /// Speed over the ground in knots
            /// </summary>
            public double SpeedKnots { get; set; }

            /// <summary>
            /// Course over the ground in degrees 
            /// </summary>
            public double CourseDegrees { get; set; }

            /// <summary>
            /// The Universal Coordinated Time (UTC) time of the fix
            /// </summary>
            public DateTime FixTimeUtc { get; set; }

            /// <summary>
            /// Provides a formatted string for this Position
            /// </summary>
            /// <returns>The formatted string.</returns>
            public override string ToString()
            {
                return "Lat " + Latitude + ", Long " + Longitude + ", Speed " + SpeedKnots + ", Course " + CourseDegrees + ", FixTime " + FixTimeUtc.ToString();
            }

            /// <summary>
            /// Implicit conversion from Position to string
            /// </summary>
            /// <param name="position">The position.</param>
            /// <returns>The string.</returns>
            public static implicit operator String(Position position) {
                if(position == null) return "NO POSITION FIX";
                return position.ToString();
            }
        }

        /// <summary>
        /// The last valid position sensed.
        /// </summary>
        public Position LastPosition { get; private set; }

        /// <summary>
        /// The elapsed duration since <see cref="LastPosition"/> was received.
        /// </summary>
        public TimeSpan LastValidPositionAge {
            get
            {
                if (validPositionTime == TimeSpan.MinValue) return TimeSpan.MaxValue;
                return GT.Timer.GetMachineTime() - validPositionTime;
            }
         }


        /// <summary>
        /// Represents the delegate that is used to handle the <see cref="PositionReceived"/> event
        /// </summary>
        /// <param name="sender">The <see cref="GPS"/> object that raised the event.</param>
        /// <param name="position">The position received</param>
        public delegate void PositionReceivedHandler(GPS sender, Position position);

        private event PositionReceivedHandler _PositionReceived;
        private int eventHandlers = 0;

        /// <summary>
        /// Triggered when a valid position is received (with a non-null Position), or when an invalid position is received after a previously valid position (with a null Position).
        /// If you handle this event (and do not use the Enabled property directly) then the GPS will be automatically enabled will automatically be set true if there is at least one active handler and false otherwise.
        /// </summary>
        public event PositionReceivedHandler PositionReceived
        {
            add
            {
                eventHandlers++;
                if (!userSetEnabled && !Enabled)
                {
                    Enabled = true;
                }
                _PositionReceived += value;
            }
            remove
            {
                eventHandlers--;
                if (!userSetEnabled && Enabled && eventHandlers == 0)
                {
                    Enabled = false;
                }
                _PositionReceived -= value;
            }
        }

        private PositionReceivedHandler onPositionReceived;

        /// <summary>
        /// Raises the <see cref="PositionReceived"/> event.
        /// </summary>
        /// <param name="sender">The <see cref="GPS"/> that raised the event.</param>
        /// <param name="position">The position received by the GPS.</param>
        protected virtual void OnPositionReceived(GPS sender, Position position)
        {
            if (this.onPositionReceived == null)
            {
                this.onPositionReceived = new PositionReceivedHandler(this.OnPositionReceived);
            }

            if (Program.CheckAndInvoke(_PositionReceived, this.onPositionReceived, sender, position))
            {
                this._PositionReceived(sender,position);
            }
        }


        /// <summary>
        /// Represents the delegate that is used to handle the <see cref="NMEASentenceReceived"/> event
        /// </summary>
        /// <param name="sender">The <see cref="GPS"/> object that raised the event.</param>
        /// <param name="nmeaSentence">The NMEA sentence received</param>
        public delegate void NMEASentenceReceivedHandler(GPS sender, String nmeaSentence);

        /// <summary>
        /// Triggered when an NMEA sentence is received.  This is for advanced users who want to parse the NMEA sentences themselves.  
        /// </summary>
        public event NMEASentenceReceivedHandler NMEASentenceReceived;

        private NMEASentenceReceivedHandler onNMEASentenceReceived;

        /// <summary>
        /// Raises the <see cref="NMEASentenceReceived"/> event.
        /// </summary>
        /// <param name="sender">The <see cref="GPS"/> that raised the event.</param>
        /// <param name="nmeaSentence">The NMEA sentence received by the GPS.</param>
        protected virtual void OnNMEASentenceReceived(GPS sender, String nmeaSentence)
        {
            if (this.onNMEASentenceReceived == null)
            {
                this.onNMEASentenceReceived = new NMEASentenceReceivedHandler(this.OnNMEASentenceReceived);
            }

            if (Program.CheckAndInvoke(NMEASentenceReceived, this.onNMEASentenceReceived, sender, nmeaSentence))
            {
                this.NMEASentenceReceived(sender, nmeaSentence);
            }
        }

        
        /// <summary>
        /// Represents the delegate that is used to handle the <see cref="InvalidPositionReceived"/> event
        /// </summary>
        /// <param name="sender">The <see cref="GPS"/> object that raised the event.</param>
        public delegate void InvalidPositionReceivedHandler(GPS sender);

        /// <summary>
        /// Triggered once when an invalid position is received for the first time (after a valid position is received).
        /// </summary>
        public event InvalidPositionReceivedHandler InvalidPositionReceived;

        private InvalidPositionReceivedHandler onInvalidPositionReceived;

        /// <summary>
        /// Raises the <see cref="InvalidPositionReceived"/> event.
        /// </summary>
        /// <param name="sender">The <see cref="GPS"/> that raised the event.</param>
        protected virtual void OnInvalidPositionReceived(GPS sender)
        {
            if (this.onInvalidPositionReceived == null)
            {
                this.onInvalidPositionReceived = new InvalidPositionReceivedHandler(this.OnInvalidPositionReceived);
            }

            if (Program.CheckAndInvoke(InvalidPositionReceived, this.onInvalidPositionReceived, sender))
            {
                this.InvalidPositionReceived(sender);
            }
        }
    }
}
